/**********************************************************************

  Audacity: A Digital Audio Editor

  ApplyMacroDialog.cpp

  Dominic Mazzoni
  James Crook

*******************************************************************//*!

\class ApplyMacroDialog
\brief Shows progress in executing commands in MacroCommands.

*//*******************************************************************/
#include "BatchProcessDialog.h"
#include "DoEffect.h"

#include <wx/setup.h> // for wxUSE_* macros

#ifdef __WXMSW__
    #include  <wx/ownerdrw.h>
#endif

#include <wx/defs.h>
#include <wx/checkbox.h>
#include <wx/choice.h>
#include <wx/frame.h>
#include <wx/log.h>
#include <wx/statbox.h>
#include <wx/stattext.h>
#include <wx/textctrl.h>
#include <wx/listctrl.h>
#include <wx/button.h>
#include <wx/imaglist.h>
#include <wx/settings.h>

#include "Clipboard.h"
#include "PluginManager.h"
#include "ShuttleGui.h"
#include "MenuCreator.h"
#include "Prefs.h"
#include "Project.h"
#include "ProjectFileManager.h"
#include "ProjectHistory.h"
#include "ProjectManager.h"
#include "ProjectWindows.h"
#include "SelectUtilities.h"
#include "Track.h"

#include "CommandManager.h"
#include "Effect.h"
#include "effects/EffectUI.h"
#include "../images/Arrow.xpm"
#include "../images/Empty9x16.xpm"
#include "UndoManager.h"
#include "Viewport.h"

#include "AllThemeResources.h"

#include "FileDialog/FileDialog.h"
#include "FileNames.h"
#include "Import.h"
#include "AudacityMessageBox.h"
#include "AudacityTextEntryDialog.h"
#include "HelpSystem.h"

#if wxUSE_ACCESSIBILITY
#include "WindowAccessible.h"
#endif

#define MacrosPaletteTitle XO("Macros Palette")
#define ManageMacrosTitle XO("Manage Macros")

// Separate numerical range from the additional buttons
// in the expanded view (which start at 10,000).
#define MacrosListID       7001
#define CommandsListID     7002
#define ApplyToProjectID   7003
#define ApplyToFilesID     7004
#define ExpandID           7005
#define ShrinkID           7006

BEGIN_EVENT_TABLE(ApplyMacroDialog, wxDialogWrapper)
EVT_BUTTON(ApplyToProjectID, ApplyMacroDialog::OnApplyToProject)
EVT_BUTTON(ApplyToFilesID, ApplyMacroDialog::OnApplyToFiles)
EVT_BUTTON(wxID_CANCEL, ApplyMacroDialog::OnCancel)
EVT_BUTTON(wxID_CLOSE, ApplyMacroDialog::OnCancel)
EVT_BUTTON(wxID_HELP, ApplyMacroDialog::OnHelp)
END_EVENT_TABLE()

ApplyMacroDialog::ApplyMacroDialog(
    wxWindow* parent, AudacityProject& project, bool bInherited)
    : wxDialogWrapper(parent, wxID_ANY, MacrosPaletteTitle,
                      wxDefaultPosition, wxDefaultSize,
                      wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER)
    , mMacroCommands{project}
    , mProject{project}
    , mCatalog(&project)
{
    mAbort = false;
    mbExpanded = false;
    if (bInherited) {
        return;
    }
    SetLabel(MacrosPaletteTitle);         // Provide visual label
    SetName(MacrosPaletteTitle);          // Provide audible label
    Populate();
}

ApplyMacroDialog::~ApplyMacroDialog()
{
}

void ApplyMacroDialog::Populate()
{
    //------------------------- Main section --------------------
    ShuttleGui S(this, eIsCreating);
    PopulateOrExchange(S);
    // ----------------------- End of main section --------------
    // Get and validate the currently active macro
    mActiveMacro = gPrefs->Read(wxT("/Batch/ActiveMacro"), wxT(""));
    // Go populate the macros list.
    PopulateMacros();

    Layout();
    Fit();
    wxSize sz = GetSize();
    SetSizeHints(sz);

    // Size and place window
    SetSize(std::min(wxSystemSettings::GetMetric(wxSYS_SCREEN_X) * 3 / 4, sz.GetWidth()),
            std::min(wxSystemSettings::GetMetric(wxSYS_SCREEN_Y) * 4 / 5, 400));

    Center();

    // Set the column size for the macros list.
    sz = mMacros->GetClientSize();
    mMacros->SetColumnWidth(0, sz.x);
}

/// Defines the dialog and does data exchange with it.
void ApplyMacroDialog::PopulateOrExchange(ShuttleGui& S)
{
    /*i18n-hint: A macro is a sequence of commands that can be applied
       * to one or more audio files.*/
    S.StartStatic(XO("Select Macro"), 1);
    {
        mMacros = S.Id(MacrosListID).Prop(1)
                  .Style(wxLC_REPORT | wxLC_HRULES | wxLC_VRULES
                         | wxLC_SINGLE_SEL)
                  // i18n-hint: This is the heading for a column in the edit macros dialog
                  .AddListControlReportMode({ XO("Macro") });
    }
    S.EndStatic();

    S.StartHorizontalLay(wxEXPAND, 0);
    {
        S.AddPrompt(XXO("Apply Macro to:"));
        wxButton* btn = S.Id(ApplyToProjectID)
                        .Name(XO("Apply macro to project"))
                        .AddButton(XXO("&Project"));
#if wxUSE_ACCESSIBILITY
        // so that name can be set on a standard control
        btn->SetAccessible(safenew WindowAccessible(btn));
#endif

        btn = S.Id(ApplyToFilesID)
              .Name(XO("Apply macro to files..."))
              .AddButton(XXO("&Files..."));
#if wxUSE_ACCESSIBILITY
        // so that name can be set on a standard control
        btn->SetAccessible(safenew WindowAccessible(btn));
#endif
    }
    S.EndHorizontalLay();

    S.StartHorizontalLay(wxEXPAND, 0);
    {
        /* i18n-hint: The Expand button makes the dialog bigger, with more in it */
        mResize = S.Id(ExpandID).AddButton(XXO("&Expand"));
        S.AddSpace(10, 10, 1);
        S.AddStandardButtons(eCloseButton | eHelpButton);
    }
    S.EndHorizontalLay();
}

/// This clears and updates the contents of mMacros, the list of macros.
/// It has cut-and-paste code from PopulateList, and both should call
/// a shared function.
void ApplyMacroDialog::PopulateMacros()
{
    auto names = mMacroCommands.GetNames();
    int i;

    int topItem = mMacros->GetTopItem();
    mMacros->DeleteAllItems();
    for (i = 0; i < (int)names.size(); i++) {
        mMacros->InsertItem(i, names[i]);
    }

    int item = mMacros->FindItem(-1, mActiveMacro);
    bool bFound = item >= 0;
    if (item == -1) {
        item = 0;
        mActiveMacro = mMacros->GetItemText(0);
    }

    // Select the name in the list...this will fire an event.
    mMacros->SetItemState(item, wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED,
                          wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED);

    if (0 <= topItem && topItem < (int)mMacros->GetItemCount()) {
        // Workaround for scrolling being windows only.
        // Try to scroll back to where we once were...
        mMacros->EnsureVisible((int)mMacros->GetItemCount() - 1);
        mMacros->EnsureVisible(topItem);
        // And then make sure whatever is selected is still visible...
        if (bFound) {
            mMacros->EnsureVisible(item);
        }
    }
}

void ApplyMacroDialog::OnHelp(wxCommandEvent& WXUNUSED(event))
{
    const auto& page = GetHelpPageName();
    HelpSystem::ShowHelp(this, page, true);
}

void ApplyMacroDialog::OnApplyToProject(wxCommandEvent& WXUNUSED(event))
{
    long item = mMacros->GetNextItem(-1,
                                     wxLIST_NEXT_ALL,
                                     wxLIST_STATE_SELECTED);

    if (item == -1) {
        AudacityMessageBox(XO("No macro selected"));
        return;
    }
    ApplyMacroToProject(item);
}

CommandID ApplyMacroDialog::MacroIdOfName(const wxString& MacroName)
{
    wxString Temp = MacroName;
    Temp.Replace(" ", "");
    Temp = wxString("Macro_") + Temp;
    return Temp;
}

// Apply macro, given its ID.
// Does nothing if not found, rather than returning an error.
void ApplyMacroDialog::ApplyMacroToProject(const CommandID& MacroID, bool bHasGui)
{
    for ( int i=0; i < mMacros->GetItemCount(); i++) {
        wxString name = mMacros->GetItemText(i);
        if (MacroIdOfName(name) == MacroID) {
            ApplyMacroToProject(i, bHasGui);
            return;
        }
    }
}

// Apply macro, given its number in the list.
void ApplyMacroDialog::ApplyMacroToProject(int iMacro, bool bHasGui)
{
    wxString name = mMacros->GetItemText(iMacro);
    if (name.empty()) {
        return;
    }

#ifdef OPTIONAL_ACTIVITY_WINDOW
    wxDialogWrapper activityWin(this, wxID_ANY, GetTitle());
    activityWin.SetName();
    ShuttleGui S(&activityWin, eIsCreating);

    S.StartHorizontalLay(wxCENTER, false);
    {
        S.StartStatic({}, false);  // deliberately not translated (!)
        {
            S.SetBorder(20);
            S.AddFixedText(XO("Applying '%s' to current project")
                           .Format(name));
        }
        S.EndStatic();
    }
    S.EndHorizontalLay();

    activityWin.Layout();
    activityWin.Fit();
    activityWin.CenterOnScreen();
    // Avoid overlap with progress.
    int x, y;
    activityWin.GetPosition(&x, &y);
    activityWin.Move(wxMax(0, x - 300), 0);
    activityWin.Show();

    // Without this the newly created dialog may not show completely.
    wxYield();
#endif

    //Since we intend to keep this dialog open, there is no reason to hide it
    //and then show it again.
    //if( bHasGui )
    //   Hide();

    gPrefs->Write(wxT("/Batch/ActiveMacro"), name);
    gPrefs->Flush();

    mMacroCommands.ReadMacro(name);

    // The disabler must get deleted before the EndModal() call.  Otherwise,
    // the menus on OSX will remain disabled.
    bool success;
    {
#ifdef OPTIONAL_ACTIVITY_WINDOW
        wxWindowDisabler wd(&activityWin);
#endif
        success = GuardedCall< bool >(
            [this]{ return mMacroCommands.ApplyMacro(mCatalog); });
    }

    if (!bHasGui) {
        return;
    }

    Show();
    Raise();
}

void ApplyMacroDialog::OnApplyToFiles(wxCommandEvent& WXUNUSED(event))
{
    long item = mMacros->GetNextItem(-1,
                                     wxLIST_NEXT_ALL,
                                     wxLIST_STATE_SELECTED);
    if (item == -1) {
        AudacityMessageBox(XO("No macro selected"));
        return;
    }

    wxString name = mMacros->GetItemText(item);
    gPrefs->Write(wxT("/Batch/ActiveMacro"), name);
    gPrefs->Flush();

    AudacityProject* project = &mProject;
    if (!TrackList::Get(*project).empty()) {
        AudacityMessageBox(
            XO("Please save and close the current project first."));
        return;
    }

    // This insures that we start with an empty and temporary project
    ProjectFileManager::Get(*project).CloseProject();
    ProjectFileManager::Get(*project).OpenProject();

    auto prompt =  XO("Select file(s) for batch processing...");

    const auto fileTypes = Importer::Get().GetFileTypes();

    auto path = FileNames::FindDefaultPath(FileNames::Operation::Open);
    FileDialogWrapper dlog(this,
                           prompt,
                           path,
                           wxT(""),
                           fileTypes,
                           wxFD_OPEN | wxFD_MULTIPLE | wxRESIZE_BORDER);

    dlog.SetFilterIndex(Importer::SelectDefaultOpenType(fileTypes));
    if (dlog.ShowModal() != wxID_OK) {
        Raise();
        return;
    }
    Raise();

    wxArrayString files;
    dlog.GetPaths(files);

    files.Sort();

    wxDialogWrapper activityWin(this, wxID_ANY, Verbatim(GetTitle()),
                                wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER
                                );
    activityWin.SetName();
    ShuttleGui S(&activityWin, eIsCreating);

    wxListCtrl* fileList = NULL;

    S.StartVerticalLay(1);
    {
        S.StartStatic(XO("Applying..."), 1);
        {
            auto imageList = std::make_unique<wxImageList>(9, 16);
            imageList->Add(wxIcon(empty9x16_xpm));
            imageList->Add(wxIcon(arrow_xpm));

            fileList = S.Id(CommandsListID)
                       .Style(wxLC_REPORT | wxLC_HRULES | wxLC_VRULES
                              | wxLC_SINGLE_SEL)
                       .AddListControlReportMode({ XO("File") });
            // AssignImageList takes ownership
            fileList->AssignImageList(imageList.release(), wxIMAGE_LIST_SMALL);
        }
        S.EndStatic();

        S.StartHorizontalLay(wxCENTER, 0);
        {
            S.Id(wxID_CANCEL).AddButton(XXO("&Cancel"));
        }
        S.EndHorizontalLay();
    }
    S.EndVerticalLay();

    int i;
    for (i = 0; i < (int)files.size(); i++ ) {
        fileList->InsertItem(i, files[i], i == 0);
    }

    // Set the column size for the files list.
    fileList->SetColumnWidth(0, wxLIST_AUTOSIZE);

    int width = wxMin(fileList->GetColumnWidth(0), 1000);
    wxSize sz = fileList->GetClientSize();
    if (sz.GetWidth() < width) {
        sz.SetWidth(width);
        if (sz.GetHeight() < width * 0.7) {
            sz.SetHeight(width * 0.7);
        }
        fileList->SetInitialSize(sz);
    }

    activityWin.Layout();
    activityWin.Fit();
    activityWin.CenterOnScreen();
    // Avoid overlap with progress.
    int x, y;
    activityWin.GetPosition(&x, &y);
    activityWin.Move(wxMax(0, x - 300), 0);
    activityWin.Show();

    // Without this the newly created dialog may not show completely.
    wxYield();
    // We could avoid hiding, but there are many dialogs on screen,
    // and hiding this one temporarily has some advantages.
    Hide();

    mMacroCommands.ReadMacro(name);
    {
        auto& globalClipboard = Clipboard::Get();

        // DV: Macro invocation on file will reset the project to the
        // initial state. There is a possibility, that clipboard will contain
        // references to the data removed
        if (globalClipboard.Project().lock().get() == project) {
            globalClipboard.Clear();
        }

        // Move global clipboard contents aside temporarily
        Clipboard::Scope scope;

        wxWindowDisabler wd(&activityWin);
        for (i = 0; i < (int)files.size(); i++) {
            if (i > 0) {
                //Clear the arrow in previous item.
                fileList->SetItemImage(i - 1, 0, 0);
            }
            fileList->SetItemImage(i, 1, 1);
            fileList->EnsureVisible(i);

            auto success = GuardedCall<bool>([&] {
                ProjectFileManager::Get(*project).Import(files[i]);
                Viewport::Get(*project).ZoomFitHorizontallyAndShowTrack(nullptr);
                SelectUtilities::DoSelectAll(*project);
                if (!mMacroCommands.ApplyMacro(mCatalog)) {
                    return false;
                }

                if (!activityWin.IsShown() || mAbort) {
                    return false;
                }

                return true;
            });

            // Ensure project is completely reset
            ProjectManager::Get(*project).ResetProjectToEmpty();
            // Bug2567:
            // Must also destroy the clipboard, to be sure sample blocks are
            // all freed and their ids can be reused safely in the next pass
            globalClipboard.Clear();

            if (!success) {
                break;
            }
        }
    }

    Show();
    Raise();
}

void ApplyMacroDialog::OnCancel(wxCommandEvent& WXUNUSED(event))
{
    Hide();
}

/////////////////////////////////////////////////////////////////////
#include <wx/textdlg.h>
#include "BatchCommandDialog.h"

enum {
    AddButtonID = 10000,
    RemoveButtonID,
    RenameButtonID,
    RestoreButtonID,
    ImportButtonID,
    ExportButtonID,
    SaveButtonID,

    DefaultsButtonID,

    InsertButtonID,
    EditButtonID,
    DeleteButtonID,
    UpButtonID,
    DownButtonID,

// MacrosListID             7005
// CommandsListID,       7002
// Re-Use IDs from ApplyMacroDialog.
    ApplyToProjectButtonID = ApplyToProjectID,
    ApplyToFilesButtonID = ApplyToFilesID,
};

BEGIN_EVENT_TABLE(MacrosWindow, ApplyMacroDialog)
EVT_LIST_ITEM_SELECTED(MacrosListID, MacrosWindow::OnMacroSelected)
EVT_LIST_ITEM_SELECTED(CommandsListID, MacrosWindow::OnListSelected)
EVT_LIST_BEGIN_LABEL_EDIT(MacrosListID, MacrosWindow::OnMacrosBeginEdit)
EVT_LIST_END_LABEL_EDIT(MacrosListID, MacrosWindow::OnMacrosEndEdit)
EVT_BUTTON(AddButtonID, MacrosWindow::OnAdd)
EVT_BUTTON(RemoveButtonID, MacrosWindow::OnRemove)
EVT_BUTTON(RenameButtonID, MacrosWindow::OnRename)
EVT_BUTTON(RestoreButtonID, MacrosWindow::OnRestore)
EVT_BUTTON(ImportButtonID, MacrosWindow::OnImport)
EVT_BUTTON(ExportButtonID, MacrosWindow::OnExport)
EVT_BUTTON(SaveButtonID, MacrosWindow::OnSave)

EVT_BUTTON(ExpandID, MacrosWindow::OnExpand)
EVT_BUTTON(ShrinkID, MacrosWindow::OnShrink)
EVT_SIZE(MacrosWindow::OnSize)

EVT_LIST_ITEM_ACTIVATED(CommandsListID, MacrosWindow::OnCommandActivated)
EVT_BUTTON(InsertButtonID, MacrosWindow::OnInsert)
EVT_BUTTON(EditButtonID, MacrosWindow::OnEditCommandParams)
EVT_BUTTON(DeleteButtonID, MacrosWindow::OnDelete)
EVT_BUTTON(UpButtonID, MacrosWindow::OnUp)
EVT_BUTTON(DownButtonID, MacrosWindow::OnDown)

EVT_BUTTON(wxID_OK, MacrosWindow::OnOK)
EVT_BUTTON(wxID_CANCEL, MacrosWindow::OnCancel)
EVT_BUTTON(wxID_CLOSE, MacrosWindow::OnCancel)

EVT_KEY_DOWN(MacrosWindow::OnKeyDown)
END_EVENT_TABLE()

enum {
    ItemNumberColumn,
    ActionColumn,
    ParamsColumn,
};

/// Constructor
MacrosWindow::MacrosWindow(
    wxWindow* parent, AudacityProject& project, bool bExpanded)
    : ApplyMacroDialog(parent, project, true)
    , mProject{project}
{
    mbExpanded = bExpanded;
    auto Title = WindowTitle();
    SetLabel(Title);    // Provide visual label
    SetName(Title);     // Provide audible label
    SetTitle(Title);

    mChanged = false;
    mSelectedCommand = 0;

    if (mbExpanded) {
        Populate();
    } else {
        ApplyMacroDialog::Populate();
    }
}

MacrosWindow::~MacrosWindow()
{
}

/// Creates the dialog and its contents.
void MacrosWindow::Populate()
{
    //------------------------- Main section --------------------
    ShuttleGui S(this, eIsCreating);
    PopulateOrExchange(S);
    // ----------------------- End of main section --------------

    // Get and validate the currently active macro
    mActiveMacro = gPrefs->Read(wxT("/Batch/ActiveMacro"), wxT(""));
    // Go populate the macros list.
    PopulateMacros();

    // We have a bare list.  We need to add columns and content.
    PopulateList();

    // Layout and set minimum size of window
    Layout();
    Fit();
    SetSizeHints(GetSize());

    // Size and place window
    SetSize(std::min(wxSystemSettings::GetMetric(wxSYS_SCREEN_X) * 3 / 4, 800),
            std::min(wxSystemSettings::GetMetric(wxSYS_SCREEN_Y) * 4 / 5, 400));
    Center();

    // Set the column size for the macros list.
    wxSize sz = mMacros->GetClientSize();
    mMacros->SetColumnWidth(0, sz.x);

    // Size columns properly
    FitColumns();
}

/// Defines the dialog and does data exchange with it.
void MacrosWindow::PopulateOrExchange(ShuttleGui& S)
{
    S.StartHorizontalLay(wxEXPAND, 1);
    {
        S.StartStatic(XO("Select Macro"), 0);
        {
            S.StartHorizontalLay(wxEXPAND, 1);
            {
                mMacros = S.Id(MacrosListID).Prop(1)
                          .Style(wxLC_REPORT | wxLC_HRULES
                                 | wxLC_SINGLE_SEL | wxLC_EDIT_LABELS)
                          // i18n-hint: This is the heading for a column in the edit macros dialog
                          .AddListControlReportMode({ XO("Macro") });
                S.StartVerticalLay(wxALIGN_TOP, 0);
                {
                    S.Id(AddButtonID).AddButton(XXO("&New"), wxALIGN_LEFT);
                    mRemove = S.Id(RemoveButtonID).AddButton(XXO("Remo&ve"), wxALIGN_LEFT);
                    mRename = S.Id(RenameButtonID).AddButton(XXO("&Rename..."), wxALIGN_LEFT);
                    mRestore = S.Id(RestoreButtonID).AddButton(XXO("Re&store"), wxALIGN_LEFT);
                    mImport = S.Id(ImportButtonID).AddButton(XO("I&mport..."), wxALIGN_LEFT);
                    mExport = S.Id(ExportButtonID).AddButton(XO("E&xport..."), wxALIGN_LEFT);
                }
                S.EndVerticalLay();
            }
            S.EndHorizontalLay();
        }
        S.EndStatic();

        S.StartStatic(XO("Edit Steps"), true);
        {
            S.StartHorizontalLay(wxEXPAND, 1);
            {
                mList = S.Id(CommandsListID)
                        .Style(wxLC_REPORT | wxLC_HRULES | wxLC_VRULES
                               | wxLC_SINGLE_SEL)
                        .AddListControlReportMode({
                    /* i18n-hint: This is the number of the command in the list */
                    { XO("Num"), wxLIST_FORMAT_RIGHT },
                    { XO("Command  "), wxLIST_FORMAT_RIGHT },
                    { XO("Parameters"), wxLIST_FORMAT_LEFT }
                });

                S.StartVerticalLay(wxALIGN_TOP, 0);
                {
                    S.Id(InsertButtonID).AddButton(XXO("&Insert"), wxALIGN_LEFT);
                    mEdit = S.Id(EditButtonID).AddButton(XXO("&Edit..."), wxALIGN_LEFT);
                    S.Id(DeleteButtonID).AddButton(XXO("De&lete"), wxALIGN_LEFT);
                    S.Id(UpButtonID).AddButton(XXO("Move &Up"), wxALIGN_LEFT);
                    S.Id(DownButtonID).AddButton(XXO("Move &Down"), wxALIGN_LEFT);
                    mSave = S.Id(SaveButtonID).AddButton(XO("&Save"), wxALIGN_LEFT);
                    mSave->Enable(mChanged);
                }
                S.EndVerticalLay();
            }
            S.EndHorizontalLay();
        }
        S.EndStatic();
    }
    S.EndHorizontalLay();

    S.StartHorizontalLay(wxEXPAND, 0);
    {
        /* i18n-hint: The Shrink button makes the dialog smaller, with less in it */
        mResize = S.Id(ShrinkID).AddButton(XXO("Shrin&k"));
        // Using variable text just to get the positioning options.
        S.Prop(0).AddVariableText(
            XO("Apply Macro to:"), false, wxALL | wxALIGN_CENTRE_VERTICAL);
        wxButton* btn = S.Id(ApplyToProjectID)
                        .Name(XO("Apply macro to project"))
                        .AddButton(XXO("&Project"));
#if wxUSE_ACCESSIBILITY
        // so that name can be set on a standard control
        btn->SetAccessible(safenew WindowAccessible(btn));
#endif

        btn = S.Id(ApplyToFilesID)
              .Name(XO("Apply macro to files..."))
              .AddButton(XXO("&Files..."));
#if wxUSE_ACCESSIBILITY
        // so that name can be set on a standard control
        btn->SetAccessible(safenew WindowAccessible(btn));
#endif
        S.AddSpace(10, 10, 1);
        // Bug 2524 OK button does much the same as cancel, so remove it.
        // OnCancel prompts you if there has been a change.
        // OnOK saves without prompting.
        // That difference is too slight to merit a button, and with the OK
        // button, people might expect the dialog to apply the macro too.
        S.AddStandardButtons(/*eOkButton |*/ eCloseButton | eHelpButton);
    }

    S.EndHorizontalLay();

    return;
}

/// This clears and updates the contents of mList, the commands for the current macro.
void MacrosWindow::PopulateList()
{
    int topItem = mList->GetTopItem();
    mList->DeleteAllItems();

    for (int i = 0; i < mMacroCommands.GetCount(); i++) {
        AddItem(mMacroCommands.GetCommand(i),
                mMacroCommands.GetParams(i));
    }
    /*i18n-hint: This is the last item in a list.*/
    AddItem(_("- END -"), wxT(""));

    // Select the name in the list...this will fire an event.
    if (mSelectedCommand >= (int)mList->GetItemCount()) {
        mSelectedCommand = 0;
    }
    mList->SetItemState(mSelectedCommand,
                        wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED,
                        wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED);
    if (0 <= topItem && topItem < (int)mList->GetItemCount()) {
        // Workaround for scrolling being windows only.
        // Try to scroll back to where we once were...
        mList->EnsureVisible((int)mList->GetItemCount() - 1);
        mList->EnsureVisible(topItem);
        // And then make sure whatever is selected is still visible...
        if (mSelectedCommand >= 0) {
            mList->EnsureVisible(mSelectedCommand);
        }
    }
}

/// Add one item into mList
void MacrosWindow::AddItem(const CommandID& Action, const wxString& Params)
{
    auto entry = mCatalog.ByCommandId(Action);
    auto friendlyName = entry != mCatalog.end()
                        ? entry->name.StrippedTranslation()
                        :
                        // uh oh, using GET to expose an internal name to the user!
                        // in default of any better friendly name
                        Action.GET();

    int i = mList->GetItemCount();

    mList->InsertItem(i, wxString::Format(wxT(" %02i"), i + 1));
    mList->SetItem(i, ActionColumn, friendlyName);
    mList->SetItem(i, ParamsColumn, Params);
}

void MacrosWindow::UpdateMenus()
{
    // OK even on mac, as dialog is modal.
    auto p = &mProject;
    MenuCreator::Get(*p).RebuildMenuBar();
}

void MacrosWindow::UpdateDisplay(bool bExpanded)
{
    // If we failed to save changes, we abandon the attempt to
    // change the expand/shrink state of the GUI.
    if (!SaveChanges()) {
        return;
    }

    mbExpanded = bExpanded;

    mChanged = false;
    // if we try to access the about to be destroyed mSave button
    // inappropriately, we need to crash rather than (sometimes) silently
    // succeed.
    mSave = nullptr;

    DestroyChildren();
    SetSizer(nullptr);

    mSelectedCommand = 0;
    SetMinSize(wxSize(200, 200));

    // Get and set position for optical stability.
    // Expanded and shrunk dialogs 'stay where they were'.
    // That's OK , and what we want, even if we exapnd off-screen.
    // We won't shrink to being off-screen, since the shrink button
    // was clicked, so must have been on screen.
    wxPoint p = GetPosition();
    if (mbExpanded) {
        Populate();
    } else {
        ApplyMacroDialog::Populate();
    }
    SetPosition(p);
    mResize->SetFocus();

    auto Title = WindowTitle();
    SetLabel(Title);          // Provide visual label
    SetName(Title);           // Provide audible label
    SetTitle(Title);
}

void MacrosWindow::OnExpand(wxCommandEvent& WXUNUSED(event))
{ UpdateDisplay(true); }

void MacrosWindow::OnShrink(wxCommandEvent& WXUNUSED(event))
{
    if (ChangeOK()) {
        UpdateDisplay(false);
    }
}

bool MacrosWindow::ChangeOK()
{
    if (mChanged) {
        int id;

        auto title = XO("%s changed").Format(mActiveMacro);
        auto msg = XO("Do you want to save the changes?");

        id = AudacityMessageBox(
            msg,
            title,
            wxYES_NO | wxCANCEL);
        if (id == wxCANCEL) {
            return false;
        }

        if (id == wxYES) {
            if (!mMacroCommands.WriteMacro(mActiveMacro)) {
                return false;
            }
        }

        mChanged = false;
        mSave->Enable(mChanged);
    }

    return true;
}

/// An item in the macros list has been selected.
void MacrosWindow::OnMacroSelected(wxListEvent& event)
{
    if (!ChangeOK()) {
        event.Veto();
        return;
    }

    int item = event.GetIndex();

    mActiveMacro = mMacros->GetItemText(item);
    ShowActiveMacro();
}

void MacrosWindow::ShowActiveMacro()
{
    mMacroCommands.ReadMacro(mActiveMacro);
    if (!mbExpanded) {
        return;
    }

    if (mMacroCommands.IsFixed(mActiveMacro)) {
        mRemove->Disable();
        mRename->Disable();
        mRestore->Enable();
    } else {
        mRemove->Enable();
        mRename->Enable();
        mRestore->Disable();
    }

    PopulateList();
}

/// An item in the macros list has been selected.
void MacrosWindow::OnListSelected(wxListEvent& event)
{
    const auto& command = mCatalog.ByTranslation(mList->GetItemText(event.GetIndex(), ActionColumn));

    if (command != mCatalog.end()) {
        PluginID ID
            =PluginManager::Get().GetByCommandIdentifier(command->name.Internal());

        mEdit->Enable(!ID.empty());
    }

    FitColumns();
}

/// The window has been resized.
void MacrosWindow::OnSize(wxSizeEvent& WXUNUSED(event))
{
    // Refresh the layout and re-fit the columns.
    Layout();
    if (!mbExpanded) {
        return;
    }
    FitColumns();
}

void MacrosWindow::FitColumns()
{
#if defined(__WXMAC__)
    // wxMac uses a hard coded width of 150 when wxLIST_AUTOSIZE_USEHEADER
    // is specified, so we calculate the width ourselves. This method may
    // work equally well on other platforms.
    for (size_t c = 0; c < mList->GetColumnCount(); c++) {
        wxListItem info;
        int width;

        mList->SetColumnWidth(c, wxLIST_AUTOSIZE);
        info.Clear();
        info.SetId(c);
        info.SetMask(wxLIST_MASK_TEXT | wxLIST_MASK_WIDTH);
        mList->GetColumn(c, info);

        mList->GetTextExtent(info.GetText(), &width, NULL);
        width += 2 * 4;  // 2 * kItemPadding - see listctrl_mac.cpp
        width += 16;     // kIconWidth - see listctrl_mac.cpp

        mList->SetColumnWidth(c, wxMax(width, mList->GetColumnWidth(c)));
    }

    // Looks strange, but it forces the horizontal scrollbar to get
    // drawn.  If not done, strange column sizing can occur if the
    // user attempts to resize the columns.
    mList->SetClientSize(mList->GetClientSize());
#else
    mList->SetColumnWidth(0, wxLIST_AUTOSIZE_USEHEADER);
    mList->SetColumnWidth(1, wxLIST_AUTOSIZE_USEHEADER);
    mList->SetColumnWidth(2, wxLIST_AUTOSIZE);
#endif

    int bestfit = mList->GetColumnWidth(2);
    int clientsize = mList->GetClientSize().GetWidth();
    int col0 = mList->GetColumnWidth(0);
    int col1 = mList->GetColumnWidth(1);
    bestfit = (bestfit > clientsize - col0 - col1) ? bestfit : clientsize - col0 - col1;
    mList->SetColumnWidth(2, bestfit);
}

///
void MacrosWindow::OnMacrosBeginEdit(wxListEvent& event)
{
    int itemNo = event.GetIndex();

    wxString macro = mMacros->GetItemText(itemNo);

    if (mMacroCommands.IsFixed(macro)) {
        wxBell();
        event.Veto();
    }
    if (mMacroBeingRenamed.IsEmpty()) {
        mMacroBeingRenamed = macro;
    }
}

///
void MacrosWindow::OnMacrosEndEdit(wxListEvent& event)
{
    if (event.IsEditCancelled()) {
        mMacroBeingRenamed = "";
        return;
    }

    if (mMacroBeingRenamed.IsEmpty()) {
        return;
    }

    wxString newname = event.GetLabel();

    mMacroCommands.RenameMacro(mMacroBeingRenamed, newname);
    if (mMacroBeingRenamed == mActiveMacro) {
        mActiveMacro = newname;
    }
    mMacroBeingRenamed="";
    PopulateMacros();
    UpdateMenus();
    event.Veto();
}

///
void MacrosWindow::OnAdd(wxCommandEvent& WXUNUSED(event))
{
    // Similar to Bug 2284 we may need to save a changed macro.
    if (!ChangeOK()) {
        return;
    }

    while (true) {
        AudacityTextEntryDialog d(this,
                                  XO("Enter name of new macro"),
                                  XO("Name of new macro"));
        d.SetName(d.GetTitle());
        wxString name;

        if (d.ShowModal() == wxID_CANCEL) {
            Raise();
            return;
        }
        Raise();

        name = d.GetValue().Strip(wxString::both);

        if (name.length() == 0) {
            AudacityMessageBox(
                XO("Name must not be blank"),
                WindowTitle(),
                wxOK | wxICON_ERROR,
                this);
            continue;
        }

        if (name.Contains(wxFILE_SEP_PATH)
            || name.Contains(wxFILE_SEP_PATH_UNIX)) {
            AudacityMessageBox(
                /*i18n-hint: The %c will be replaced with 'forbidden characters', like '/' and '\'.*/
                XO("Names may not contain '%c' and '%c'")
                .Format(wxFILE_SEP_PATH, wxFILE_SEP_PATH_UNIX),
                WindowTitle(),
                wxOK | wxICON_ERROR,
                this);
            continue;
        }

        mMacroCommands.AddMacro(name);

        mActiveMacro = name;

        PopulateMacros();
        UpdateMenus();

        break;
    }
}

///
void MacrosWindow::OnRemove(wxCommandEvent& WXUNUSED(event))
{
    long item = mMacros->GetNextItem(-1,
                                     wxLIST_NEXT_ALL,
                                     wxLIST_STATE_SELECTED);
    if (item == -1) {
        return;
    }

    wxString name = mMacros->GetItemText(item);
    AudacityMessageDialog m(
        this,
        /*i18n-hint: %s will be replaced by the name of a file.*/
        XO("Are you sure you want to delete %s?").Format(name),
        Verbatim(GetTitle()),
        wxYES_NO | wxICON_QUESTION);
    if (m.ShowModal() == wxID_NO) {
        Raise();
        return;
    }
    Raise();

    mMacroCommands.DeleteMacro(name);

    item++;
    if (item >= (mMacros->GetItemCount() - 1) && item >= 0) {
        item--;
    }

    // Bug 2284.  The macro we have just removed might have been
    // changed.  Since we've just deleted the macro, we should
    // forget about that change.
    mChanged = false;
    mSave->Enable(mChanged);
    mActiveMacro = mMacros->GetItemText(item);

    PopulateMacros();
    UpdateMenus();
}

///
void MacrosWindow::OnRename(wxCommandEvent& WXUNUSED(event))
{
    long item = mMacros->GetNextItem(-1,
                                     wxLIST_NEXT_ALL,
                                     wxLIST_STATE_SELECTED);
    if (item == -1) {
        return;
    }

    mMacros->EditLabel(item);
    UpdateMenus();
}

/// Reset a built in macro.
void MacrosWindow::OnRestore(wxCommandEvent& WXUNUSED(event))
{
    mMacroCommands.RestoreMacro(mActiveMacro);

    mChanged = true;
    mSave->Enable(mChanged);

    PopulateList();
}

///
void MacrosWindow::OnImport(wxCommandEvent& WXUNUSED(event))
{
    if (!ChangeOK()) {
        return;
    }

    long item = mMacros->GetNextItem(-1,
                                     wxLIST_NEXT_ALL,
                                     wxLIST_STATE_SELECTED);
    if (item == -1) {
        return;
    }

    wxString name = mMacros->GetItemText(item);

    name = mMacroCommands.ReadMacro({}, this);
    if (name == wxEmptyString) {
        return;
    }

    mActiveMacro = name;

    PopulateMacros();
    UpdateMenus();
}

///
void MacrosWindow::OnExport(wxCommandEvent& WXUNUSED(event))
{
    long item = mMacros->GetNextItem(-1,
                                     wxLIST_NEXT_ALL,
                                     wxLIST_STATE_SELECTED);
    if (item == -1) {
        return;
    }

    mMacroCommands.WriteMacro(mMacros->GetItemText(item), this);
}

void MacrosWindow::OnSave(wxCommandEvent& WXUNUSED(event))
{
    SaveChanges();
}

/// An item in the list has been selected.
/// Bring up a dialog to allow its parameters to be edited.
void MacrosWindow::OnCommandActivated(wxListEvent& WXUNUSED(event))
{
    wxCommandEvent dummy;
    OnEditCommandParams(dummy);
}

///
void MacrosWindow::OnInsert(wxCommandEvent& WXUNUSED(event))
{
    long item = mList->GetNextItem(-1,
                                   wxLIST_NEXT_ALL,
                                   wxLIST_STATE_SELECTED);
    if (item == -1) {
        item = mList->GetItemCount() - 1;
    }
    InsertCommandAt(item);
}

void MacrosWindow::InsertCommandAt(int item)
{
    if (item == -1) {
        return;
    }

    MacroCommandDialog d(this, wxID_ANY, mProject);

    if (!d.ShowModal()) {
        Raise();
        return;
    }
    Raise();

    if (!d.mSelectedCommand.empty()) {
        mMacroCommands.AddToMacro(d.mSelectedCommand,
                                  d.mSelectedParameters,
                                  item);
        mChanged = true;
        mSave->Enable(mChanged);

        mSelectedCommand = item + 1;
        PopulateList();
    }
}

void MacrosWindow::OnEditCommandParams(wxCommandEvent& WXUNUSED(event))
{
    int item = mList->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);

    // LAST command in list is END.
    // If nothing selected, add at END.
    // If END selected, add at END.
    // When adding at end we use InsertCommandAt, so that a new command
    // can be chosen.
    int lastItem = mList->GetItemCount() - 1;
    if ((item < 0) || (item + 1) == mList->GetItemCount()) {
        InsertCommandAt(lastItem);
        return;
    }

    // Just edit the parameters, and not the command.
    auto command = mMacroCommands.GetCommand(item);
    wxString params  = mMacroCommands.GetParams(item);
    wxString oldParams = params;

    params = MacroCommands::PromptForParamsFor(command, params, mProject).Trim();
    Raise();

    if (oldParams == params) {
        return; // They did not actually make any changes..
    }
    mMacroCommands.DeleteFromMacro(item);
    mMacroCommands.AddToMacro(command,
                              params,
                              item);

    mChanged = true;
    mSave->Enable(mChanged);

    mSelectedCommand = item;
    PopulateList();
}

///
void MacrosWindow::OnDelete(wxCommandEvent& WXUNUSED(event))
{
    long item = mList->GetNextItem(-1,
                                   wxLIST_NEXT_ALL,
                                   wxLIST_STATE_SELECTED);
    if (item == -1 || item + 1 == mList->GetItemCount()) {
        return;
    }

    mMacroCommands.DeleteFromMacro(item);

    mChanged = true;
    mSave->Enable(mChanged);

    if (item >= (mList->GetItemCount() - 2) && item >= 0) {
        item--;
    }
    mSelectedCommand = item;
    PopulateList();
}

///
void MacrosWindow::OnUp(wxCommandEvent& WXUNUSED(event))
{
    long item = mList->GetNextItem(-1,
                                   wxLIST_NEXT_ALL,
                                   wxLIST_STATE_SELECTED);
    if (item == -1 || item == 0 || item + 1 == mList->GetItemCount()) {
        return;
    }

    mMacroCommands.AddToMacro(mMacroCommands.GetCommand(item),
                              mMacroCommands.GetParams(item),
                              item - 1);
    mMacroCommands.DeleteFromMacro(item + 1);

    mChanged = true;
    mSave->Enable(mChanged);

    mSelectedCommand = item - 1;
    PopulateList();
}

///
void MacrosWindow::OnDown(wxCommandEvent& WXUNUSED(event))
{
    long item = mList->GetNextItem(-1,
                                   wxLIST_NEXT_ALL,
                                   wxLIST_STATE_SELECTED);
    if (item == -1 || item + 2 >= mList->GetItemCount()) {
        return;
    }

    mMacroCommands.AddToMacro(mMacroCommands.GetCommand(item),
                              mMacroCommands.GetParams(item),
                              item + 2);
    mMacroCommands.DeleteFromMacro(item);

    mChanged = true;
    mSave->Enable(mChanged);

    mSelectedCommand = item + 1;
    PopulateList();
}

void MacrosWindow::OnApplyToProject(wxCommandEvent& event)
{
    if (!SaveChanges()) {
        return;
    }
    ApplyMacroDialog::OnApplyToProject(event);
}

void MacrosWindow::OnApplyToFiles(wxCommandEvent& event)
{
    if (!SaveChanges()) {
        return;
    }
    ApplyMacroDialog::OnApplyToFiles(event);
}

bool MacrosWindow::SaveChanges()
{
    gPrefs->Write(wxT("/Batch/ActiveMacro"), mActiveMacro);
    gPrefs->Flush();

    if (mChanged) {
        if (!mMacroCommands.WriteMacro(mActiveMacro)) {
            return false;
        }
    }

    mChanged = false;
    if (mSave) {
        mSave->Enable(mChanged);
    }

    return true;
}

/// Send changed values back to Prefs, and update Audacity.
void MacrosWindow::OnOK(wxCommandEvent& WXUNUSED(event))
{
    if (!SaveChanges()) {
        return;
    }
    Hide();
    //EndModal(true);
}

///
void MacrosWindow::OnCancel(wxCommandEvent& WXUNUSED(event))
{
    bool bWasChanged = mChanged;
    if (!ChangeOK()) {
        return;
    }
    // If we've rejected a change, we need to restore the display
    // of the active macro.
    // That's because next time we open this dialog we want to see the
    // unedited macro.
    if (bWasChanged) {
        ShowActiveMacro();
    }
    Hide();
}

///
void MacrosWindow::OnKeyDown(wxKeyEvent& event)
{
    if (event.GetKeyCode() == WXK_DELETE) {
        wxLogDebug(wxT("wxKeyEvent"));
    }

    event.Skip();
}

TranslatableString MacrosWindow::WindowTitle() const
{
    return mbExpanded ? ManageMacrosTitle : MacrosPaletteTitle;
}

// PrefsListener implementation
void MacrosWindow::UpdatePrefs()
{
    UpdateDisplay(mbExpanded);
}

// The rest of this file installs hooks

#include "CommonCommandFlags.h"
#include "CommandContext.h"
namespace {
AttachedWindows::RegisteredFactory sMacrosWindowKey{
    []( AudacityProject& parent ) -> wxWeakRef< wxWindow > {
        auto& window = GetProjectFrame(parent);
        return safenew MacrosWindow(
            &window, parent, true
            );
    }
};

using MacroID = wxString;

void OnApplyMacroDirectlyByName(
    const CommandContext& context, const MacroID& Name);

void OnRepeatLastTool(const CommandContext& context)
{
    auto& commandManager = CommandManager::Get(context.project);
    switch (commandManager.mLastToolRegistration) {
    case CommandManager::repeattypeplugin:
    {
        auto lastEffect = commandManager.mLastTool;
        if (!lastEffect.empty()) {
            EffectUI::DoEffect(
                lastEffect, context.project, commandManager.mRepeatToolFlags);
        }
    }
    break;
    case CommandManager::repeattypeunique:
        CommandManager::Get(context.project).DoRepeatProcess(context,
                                                             commandManager.mLastToolRegisteredId);
        break;
    case CommandManager::repeattypeapplymacro:
        OnApplyMacroDirectlyByName(context, commandManager.mLastTool);
        break;
    }
}

void OnManageMacros(const CommandContext& context)
{
    auto& project = context.project;
    CommandManager::Get(project).RegisterLastTool(context); //Register Macros as Last Tool
    auto macrosWindow = &GetAttachedWindows(project)
                        .AttachedWindows::Get< MacrosWindow >(sMacrosWindowKey);
    if (macrosWindow) {
        macrosWindow->Show();
        macrosWindow->Raise();
        macrosWindow->UpdateDisplay(true);
    }
}

void OnApplyMacrosPalette(const CommandContext& context)
{
    auto& project = context.project;
    CommandManager::Get(project).RegisterLastTool(context); //Register Palette as Last Tool
    auto macrosWindow = &GetAttachedWindows(project)
                        .AttachedWindows::Get< MacrosWindow >(sMacrosWindowKey);
    if (macrosWindow) {
        macrosWindow->Show();
        macrosWindow->Raise();
        macrosWindow->UpdateDisplay(false);
    }
}

void OnApplyMacroDirectly(const CommandContext& context)
{
    const MacroID& Name = context.parameter.GET();
    OnApplyMacroDirectlyByName(context, Name);
}

void OnApplyMacroDirectlyByName(const CommandContext& context, const MacroID& Name)
{
    auto& project = context.project;
    auto& window = GetProjectFrame(project);
    //wxLogDebug( "Macro was: %s", context.parameter);
    ApplyMacroDialog dlg(&window, project);
    //const auto &Name = context.parameter;

// We used numbers previously, but macros could get renumbered, making
// macros containing macros unpredictable.
#ifdef MACROS_BY_NUMBERS
    long item=0;
    // Take last three letters (of e.g. Macro007) and convert to a number.
    Name.Mid(Name.length() - 3).ToLong(&item, 10);
    dlg.ApplyMacroToProject(item, false);
#else
    dlg.ApplyMacroToProject(Name, false);
#endif
    /* i18n-hint: %s will be the name of the macro which will be
     * repeated if this menu item is chosen */
    CommandManager::Get(project).ModifyUndoMenuItems();

    TranslatableString desc;
    auto shortDesc = PluginManager::Get().GetName(Name);
    auto& undoManager = UndoManager::Get(project);
    auto& commandManager = CommandManager::Get(project);
    int cur = undoManager.GetCurrentState();
    if (undoManager.UndoAvailable()) {
        undoManager.GetShortDescription(cur, &desc);
        commandManager.Modify(wxT("RepeatLastTool"), XXO("&Repeat %s")
                              .Format(desc));
        auto& commandManager = CommandManager::Get(project);
        commandManager.mLastTool = Name;
        commandManager.mLastToolRegistration
            =CommandManager::repeattypeapplymacro;
    }
}

void PopulateMacrosMenu(MenuRegistry::MenuItems& items, CommandFlag flags)
{
    using namespace MenuRegistry;
    auto names = MacroCommands::GetNames(); // these names come from filenames
    for (const auto& name : names) {
        auto MacroID = ApplyMacroDialog::MacroIdOfName(name);
        items.push_back(MenuRegistry::Command(MacroID,
                                              Verbatim(name), // file name verbatim
                                              OnApplyMacroDirectly,
                                              flags,
                                              Options {}.AllowInMacros()
                                              ));
    }
}

const ReservedCommandFlag&
HasLastToolFlag()
{
    static ReservedCommandFlag flag{
        [](const AudacityProject& project) {
            auto& commandManager = CommandManager::Get(project);
            if (commandManager.mLastToolRegistration
                == CommandManager::repeattypeunique) {
                return true;
            }
            return !commandManager.mLastTool.empty();
        }
    };
    return flag;
}
}

using namespace MenuRegistry;

auto PluginMenuItems()
{
    static auto items = std::shared_ptr{
        Items("Macros",
              Section("RepeatLast",
                      // Delayed evaluation:
                      [](AudacityProject& project)
        {
            const auto& lastTool = CommandManager::Get(project).mLastTool;
            TranslatableString buildMenuLabel;
            if (!lastTool.empty()) {
                buildMenuLabel
                    =XO("Repeat %s")
                      .Format(PluginManager::Get().GetName(lastTool));
            } else {
                buildMenuLabel = XO("Repeat Last Tool");
            }

            return Command(wxT("RepeatLastTool"), buildMenuLabel,
                           OnRepeatLastTool,
                           AudioIONotBusyFlag()
                           | HasLastToolFlag(),
                           Options {}.IsGlobal());
        }
                      ),

              Command(wxT("ManageMacros"), XXO("&Macro Manager"),
                      OnManageMacros, AudioIONotBusyFlag()),

              Menu(wxT("Macros"), XXO("&Apply Macro"),
                   // Palette has no access key to ensure first letter navigation of
                   // sub menu
                   Section("",
                           Command(wxT("ApplyMacrosPalette"), XXO("Palette..."),
                                   OnApplyMacrosPalette, AudioIONotBusyFlag())
                           ),

                   Section("",
                           // Delayed evaluation:
                           [](AudacityProject&) {
            auto result = Items("");
            PopulateMacrosMenu(*result, AudioIONotBusyFlag());
            return result;
        }
                           )
                   )
              ) };
    return items;
}

AttachedItem sAttachment1{ Indirect(PluginMenuItems()), wxT("Tools/Manage") };

auto ExtraScriptablesIMenu()
{
    // These are the more useful to VI user Scriptables.
    static auto menu = std::shared_ptr{
        // i18n-hint: Scriptables are commands normally used from Python, Perl etc.
        Menu(wxT("Scriptables1"), XXO("Script&ables I"))
    };
    return menu;
}

AttachedItem sAttachment2{ Indirect(ExtraScriptablesIMenu()),
                           wxT("Optional/Extra/Part2")
};

auto ExtraScriptablesIIMenu()
{
    // Less useful to VI users.
    static auto menu = std::shared_ptr{
        // i18n-hint: Scriptables are commands normally used from Python, Perl etc.
        Menu(wxT("Scriptables2"), XXO("Scripta&bles II"))
    };
    return menu;
}

AttachedItem sAttachment3{ Indirect(ExtraScriptablesIIMenu()),
                           wxT("Optional/Extra/Part2")
};
